package cz.cuni.mff.ufal.volk;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import cz.cuni.mff.ufal.httpserver.HttpRequestHandler;
import cz.cuni.mff.ufal.httpserver.protocol.HttpRequest;
import cz.cuni.mff.ufal.httpserver.protocol.HttpResponse;
import cz.cuni.mff.ufal.httpserver.protocol.HttpResponse.StandardHeader;
import cz.cuni.mff.ufal.httpserver.protocol.HttpResponse.Status;
import cz.cuni.mff.ufal.volk.boot.BootCode;
import cz.cuni.mff.ufal.volk.boot.BootCodeLoader;
import cz.cuni.mff.ufal.volk.boot.BootCodeLoaderMock;
import cz.cuni.mff.ufal.volk.command.Command;
import cz.cuni.mff.ufal.volk.command.MessageCommand;
import cz.cuni.mff.ufal.volk.events.NabaztagEventListener;
import cz.cuni.mff.ufal.volk.events.NabaztagServerEventListener;
import cz.cuni.mff.ufal.volk.message.IdMessage;
import cz.cuni.mff.ufal.volk.message.WaitMessage;
import cz.cuni.mff.ufal.volk.utils.Utils;

/**
 *
 * @author Bartłomiej Etenkowski
 */
public class NabaztagRequestHandler implements HttpRequestHandler, NabaztagHttpListener {

	public NabaztagRequestHandler(Collection<NabaztagEventListener> listeners) {
		listeners.getClass();
		this.listeners = listeners;
	}

	private final Collection<NabaztagEventListener> listeners;

  //[start] logger
  private static org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(NabaztagRequestHandler.class);
  //[end]

  @Override
  public void process(HttpRequest request, HttpResponse response) {
    String path = request.getRequestedUri().getPath();
    NabaztagResource rh = NabaztagResource.getByPath(path);
    log.info("request: " + path);
    if (rh == null) {
      log.warn(String.format("not found [%s]", path));
      response.setStatus(Status.NOT_FOUND);
    } else {
      rh.process(request, response, this);
    }
  }

  /**
   * Map of the nabaztags that are logged-in into the server. The serial numbers are the keys.
   * The access to the map should be synchronized by {@code synchronized(nabaztags)}. The map
   * must not contain null values.
   */
  private final Map<String, NabaztagProxy> nabaztags = new HashMap<String, NabaztagProxy>();

  public final Collection<NabaztagProxy> getRegisteredBunnies() {
  	return new ArrayList<NabaztagProxy>(nabaztags.values());
  }

  /**
   * <p>The method looks for the nabaztag with the given serial number in the {@code #nabaztags} map.
   * If the nabaztag with the given serial number does not exist in the map it is created
   * and cached.</p>
   * <p>The method is thread safe.</p>
   * @param serial the serial number of the nabaztag
   * @return the found or just created nabaztag
   */
  private NabaztagProxy getNabaztag(String serial) {
    boolean added = false;
    NabaztagProxy nab;
    synchronized (nabaztags) {
      nab = nabaztags.get(serial);
      if (nab == null) {
        nab = new NabaztagProxy(serial, tempDirectory.getAbsolutePath());
        nabaztags.put(serial, nab);
        added = true;
      }
    }
    if (added) {
      for (NabaztagServerEventListener listener : eventListeners) {
        listener.registered(nab);
      }
      synchronized (listeners) {
      	for (NabaztagEventListener lst : listeners) {
      		lst.onStarted(nab);
      	}
      }
      log.info(String.format("Nabaztag %s has been added to the collection", serial));
    }
    return nab;
  }

  private final List<NabaztagServerEventListener> eventListeners = new ArrayList<NabaztagServerEventListener>();

  /**
   * Retrieves the list of {@code NabServerEventListener} of this server. The list must not be modified
   * when the server is running.
   *
   * @return the list of listeners
   */
  public List<NabaztagServerEventListener> getEventListeners() {
    return eventListeners;
  }

  private BootCodeLoader bootCodeLoader = new BootCodeLoaderMock();

  public BootCodeLoader getBootCodeLoader() {
    return bootCodeLoader;
  }

  public void setBootCodeLoader(BootCodeLoader bootCodeLoader) {
    if (bootCodeLoader == null)
      throw new NullPointerException("The bootCodeLoader argument is null");

    this.bootCodeLoader = bootCodeLoader;
  }

  private boolean removeTempFiles = true;

  public boolean getRemoveTempFiles() {
  	return removeTempFiles;
  }

  public void setRemoveTempFiles(boolean value) {
  	removeTempFiles = value;
  }

  @Override
  public void boot(HttpRequest request, HttpResponse response) throws MalformedRequestException {
    Map<String, String> queryParameters = request.getQueryParameters();
    String version = requireParameter(queryParameters, "v");
    String mac = requireParameter(queryParameters, "m");
    String serial = mac.replaceAll(":", "");
    log.info(String.format("bootcode, version=%s, mac=%s", version, mac));

    getNabaztag(serial);
    BootCode bootCode = bootCodeLoader.loadBootCode(version, serial);
    response.putHeader(StandardHeader.CONTENT_LENGTH, Long.toString(bootCode.size()));
    response.writeHeaders();
    try {
      InputStream bootCodeStream = bootCode.getInputStream();
      try {
        Utils.copy(bootCodeStream, response.getOutputStream(), 1000);
      } finally {
        bootCodeStream.close();
      }
    } catch (IOException e) {
      log.error("bootcode sending failure", e);
    }
  }

  private String pingServerAddress = "127.0.0.1";

  public void setPingServerAddress(String pingServerAddress) {
    this.pingServerAddress = pingServerAddress;
  }

  public String getPingServerAddress() {
    return pingServerAddress;
  }


  private String broadcastServerAddress = "127.0.0.1";

  public void setBroadcastServerAddress(String broadcastServerAddress) {
    this.broadcastServerAddress = broadcastServerAddress;
  }

  public String getBroadcastServerAddress() {
    return broadcastServerAddress;
  }

  private File tempDirectory;

  public void setTempDir(String path) {
    if (path == null)
      tempDirectory = null;
    else
      tempDirectory = new File(path);
  }

  public void setTempDirectory(File tempDir) {
    this.tempDirectory = tempDir;
  }

  public File getTempDir() {
    return tempDirectory;
  }

  /**
   * Sets both ping and broadcast server IP.
   * @param serverAddress the IP address on which the server is accessible in the network
   */
  public void setServerAddress(String serverAddress) {
  	log.trace("Setting server address: " + serverAddress);
    pingServerAddress = serverAddress;
    broadcastServerAddress = serverAddress;
  }

  @Override
  public void locate(HttpRequest request, HttpResponse response) throws MalformedRequestException {
    PrintWriter cout = response.getPrintWriter();

    Map<String, String> queryParameters = request.getQueryParameters();
    String serial = requireParameter(queryParameters, "sn");
    log.info(String.format("locate, serial=%s", serial));
    log.info(String.format("ping     =%s", pingServerAddress));
    log.info(String.format("broadcast=%s", broadcastServerAddress));
    getNabaztag(serial);

    response.writeHeaders();
    cout.print(String.format("ping %s\r\n", pingServerAddress));
    cout.print(String.format("broad %s\r\n\r\n", broadcastServerAddress));
  }

  private byte[] createCommandBytes(NabaztagImpl nab) throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
    Command cmd;
    while ((cmd = nab.dequeueCommand()) != null) {
    	log.info("sending command: " + cmd.toString());
      cmd.writeCommand(out);
    }
    return out.toByteArray();
  }

  @Override
  public void ping(HttpRequest request, HttpResponse response) throws MalformedRequestException {
    Map<String, String> queryParameters = request.getQueryParameters();
    for (Entry<String, String> e : queryParameters.entrySet())
    	log.trace(String.format("param(%s)=%s", e.getKey(), e.getValue()));

    String serial = requireParameter(queryParameters, "sn");
    String actionString = requireParameter(queryParameters, "sd");

    if (actionString.equals("")) {
      throw new MalformedRequestException("The sd parameter is empty");
    }

    NabaztagProxy nab = getNabaztag(serial);
    UserAction action = UserAction.byCharacterId(actionString.charAt(0));
    if (action == null) {
      throw new MalformedRequestException("Unsupported user action");
    } else {
      log.info(String.format("%s, serial=%s", action, serial));
      action.process(listeners, nab, actionString.substring(1));
    }

    try {
      byte[] commandBytes = createCommandBytes(nab.getBunny());
      final byte[] blockStart = new byte[] { (byte)0x7f };
      final byte[] blockEnd = new byte[] { (byte)0xff, (byte)0x0a };
      if (commandBytes.length == 0 && (justRecorded || action != UserAction.COMPLETED)) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        new MessageCommand(new IdMessage(1), new WaitMessage()).writeCommand(out);
        commandBytes = out.toByteArray();
        justRecorded = false;
      }

      response.putHeader(StandardHeader.CONTENT_LENGTH, Integer.toString(commandBytes.length
          + blockStart.length + blockEnd.length));
      response.writeHeaders();
      //writeCommands(nab, response.getOutputStream());
      OutputStream out = response.getOutputStream();
      out.write(blockStart);
      out.write(commandBytes);
      out.write(blockEnd);
      log.info(String.format("%d bytes sent to Nabaztag", commandBytes.length));
    } catch (IOException e) {
      log.error("ping processing failure", e);
    }
  }

  private boolean justRecorded = false;

  @Override
  public void record(HttpRequest request, HttpResponse response) throws MalformedRequestException {
    Map<String, String> queryParameters = request.getQueryParameters();
    String serial = requireParameter(queryParameters, "sn");
    log.info(String.format("RECORD, serial=%s", serial));
    log.trace("temp directory=" + tempDirectory);

    NabaztagProxy nab = getNabaztag(serial);
    if (listeners.size() == 0) {
      log.info("no listener registered, ignoring the sound");
    } else {
      try {
        // The Nabaztag sends small wave files only when it records sound. Therefore the files can be saved
        // as temporary files.
        File temp = File.createTempFile("sound", ".wav", tempDirectory);
        temp.deleteOnExit(); // TODO try to remove the file after all streams pointing to it have been closed
        OutputStream out = new FileOutputStream(temp);
        try {
          Utils.copy(request.getInputStream(), out);
        } finally {
          out.close();
        }
        response.writeHeaders();
        for (NabaztagEventListener listener : listeners) {
          Speech speech = new Speech(null, "wav/adpcm", Utils.readAllBytes(temp));
          listener.onSoundRecorded(nab, speech);
        }
        justRecorded = true;
      } catch (IOException e) {
        log.error("an I/O exception occurred", e);
      }
    }
  }

  @Override
  public void rfid(HttpRequest request, HttpResponse response) {
    // TODO Auto-generated method stub
  }

  /** The only local directory accessible by the Nabaztag. */
  private File localDirectory;

  public File getLocalDirectory() {
    return localDirectory;
  }

  public void setLocalDirectory(File localDirectory) {
    this.localDirectory = localDirectory;
  }

  private final static String TEMP_PREFIX = "temp/";

  @Override
  public void localResource(HttpRequest request, HttpResponse response, String resource) {
    File directory; // the directory to read from
    boolean shouldDelete = false; // whether the resource should be removed after sending
    if (resource.startsWith(TEMP_PREFIX)) {
      shouldDelete = removeTempFiles;
      directory = tempDirectory;
      resource = resource.substring(TEMP_PREFIX.length());
      log.info("temporary resource request: " + resource);
    } else {
      directory = localDirectory;
      log.info("local resource request: " + resource);
    }
    if (directory == null) {
      response.setStatus(Status.FORBIDDEN);
      response.writeHeaders();
    } else {
      if (directory.isDirectory()) {
        try {
          sendLocalFile(response, resource, directory, shouldDelete);
        } catch (IOException e) {
          log.error("cannot send local file, an I/O exception occurred", e);
        }
      } else {
        response.setStatus(Status.INTERNAL_SERVER_ERROR);
        response.writeHeaders();
      }
    }
  }

  private void sendLocalFile(HttpResponse response, String resource, File directory,
      boolean shouldDelete) throws FileNotFoundException, IOException {
    File requestedFile = Auxiliary.getLocalResource(directory, resource);
    if (requestedFile == null) {
      response.setStatus(Status.NOT_FOUND);
      response.writeHeaders();
    } else {
      response.setStatus(Status.OK);
      long size = requestedFile.length();
      response.putHeader(StandardHeader.CONTENT_LENGTH, Long.toString(size));
      response.writeHeaders();
      InputStream is = new FileInputStream(requestedFile);
      try {
        Utils.copy(is, response.getOutputStream());
      } finally {
        is.close();
      }
      log.info(String.format("%d bytes written", size));
      if (shouldDelete) {
        requestedFile.delete();
        log.info("temporary resource removed: " + requestedFile.getName());
      }
    }
  }

  /**
   * Looks for the value associated with the given key in the map. If the map does not contain
   * any value associated with the key, an exception of the type
   * {@code MalformedRequestException} is thrown.
   *
   * @param  <K> the type of keys maintained by the map
   * @param  <V> the type of mapped values
   * @param  map the map
   * @param  key the key whose associated value is to be returned
   * @return the value associated with the key
   * @throws MalformedRequestException if the map contains no value associated with the key
   */
  private static <K, V> V requireParameter(Map<K, V> map, K key) throws MalformedRequestException {
    if (map.containsKey(key)) {
      return map.get(key);
    } else {
      throw new MalformedRequestException(String.format("Missing parameter: %s", key.toString()));
    }
  }

}
